/* MIT License
 *
 * Copyright (c) 2012 Reuben Smith, Miami University
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.droidlike.vsig;


import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.logging.Level;
import java.util.logging.Logger;


// TODO Optimize, optimize, optimize.
// TODO Refactor this out into a SerialExecutor to allow for an MT version later.

/**
 * Verilog script executor service. Allows for the synthesis and simulation of
 * many managed Verilog scripts.
 *
 * @author smithre5
 */
class ExecutorService
{
    /**
     * Creates an executor service with the given paths. The service path refers
     * to the executable that performs the actual synthesis and simulation. This
     * will almost always be ODIN II. The storage path is where temporary files
     * generated by the service will be stored. This should refer to a directory
     * that does NOT exist as the directory and all its contents will be deleted
     * once the service is destroyed.
     *
     * @param servicePath the service executable
     * @param storagePath the service's temporary storage directory
     *
     * @return an Executor service if successful; null otherwise
     */
    public static ExecutorService create(String servicePath, String storagePath)
    {
        if (Util.isBlank(servicePath) || Util.isBlank(storagePath)) {
            return null;
        }

        File serviceFile = new File(servicePath);

        if (!serviceFile.canExecute()) {
            return null;
        }

        File storageFile = new File(storagePath);

        if (!storageFile.exists()) {
            if (!storageFile.mkdir()) {
                return null;
            }

            // smithre5: Not entirely sure if there's some permission setup that
            // would allow this to happen, but better safe than sorry.
            if (!storageFile.canRead()) {
                storageFile.delete();
                return null;
            }
        }

        ExecutorService service = new ExecutorService();
        service.servicePath = servicePath;
        service.storagePath = storagePath;
        service.service = new ProcessBuilder(servicePath);
        service.rawModules = new ArrayList<String>();
        service.readyModules = new HashMap<Integer, ReadyModule>();

        return service;
    }


    /**
     * Cleans up all service temporaries and puts the service object in an
     * unusable state.
     */
    public void destroy()
    {
        File storageFile = new File(storagePath);
        for (File tempFile : storageFile.listFiles()) {
            tempFile.delete();
        }
        storageFile.delete();

        servicePath = null;
        storagePath = null;
        service = null;
        rawModules = null;
        readyModules = null;
    }


    /**
     * Adds a raw (unsynthesized) module to the service's module pool.
     *
     * @param modulePath the Verilog script to add to the module pool
     *
     * @return a positive module ID if successful; a negative number otherwise
     */
    public int addModule(String modulePath)
    {
        if (Util.isBlank(modulePath)) {
            return -1;
        }

        File moduleFile = new File(modulePath);

        if (!moduleFile.canRead()) {
            return -2;
        }

        int moduleID = rawModules.indexOf(modulePath);

        if (moduleID >= 0) {
            return moduleID;
        }

        if (!rawModules.add(modulePath)) {
            return -3;
        }

        return rawModules.size() - 1;
    }


    /**
     * Simulates a module with the given input. The input given does NOT have to
     * correspond to the actual width of the input signals. This is based on the
     * default simulator, ODIN II, ignoring spurious data. Support for this COULD
     * break in the future. Users should future-proof input to avoid this. Given
     * that a long can only represent 64b of data, input widths larger than this
     * are not currently supported.
     *
     * @param moduleID the module to be simulated
     * @param input    the input to be passed
     *
     * @return the resulting output; can be null
     */
    public long[] callModule(int moduleID, long[] input)
    {
        // TODO Optimize, optimize, optimize.
        // TODO Improve error handling.

        if (!Util.inRange(moduleID, 0, rawModules.size() - 1)) {
            return null;
        }

        File vectorsFile = null;

        try {
            vectorsFile = File.createTempFile("vsig", "", new File(storagePath));
        }
        catch (Exception ex) {
            System.err.println(ex);
            return null;
        }

        ReadyModule module = readyModules.get(moduleID);

        if (input != null && input.length > 0) {
            try {
                BufferedWriter writer = new BufferedWriter(new FileWriter(vectorsFile));

                // smithre5: In theory a lot of inputs could be given, hence using
                // StringBuilder over just concatenating them. In practice, this
                // probably doesn't gain much.

                // Shift by 2 since the average input name will probably be around
                // 4 characters in size.
                StringBuilder headerString = new StringBuilder(module.inputs.length << 2);

                for (String inputName : module.inputs) {
                    headerString.append(inputName);
                    headerString.append(' ');
                }
                headerString.append('\n');
                writer.write(headerString.toString());

                // Shift by 4 since the largest possible input is around 16 characters
                // in size.
                StringBuilder inputString = new StringBuilder(module.inputs.length << 4);

                for (long inputValue : input) {
                    inputString.append("0X");
                    inputString.append(Long.toHexString(inputValue).toUpperCase());
                    inputString.append(' ');
                }
                inputString.append('\n');
                writer.write(inputString.toString());

                writer.close();

                service.command(servicePath,
                                "-b", module.modulePath,
                                "-O", vectorsFile.getPath(),
                                "-t", vectorsFile.getPath()).start().waitFor();
            }
            catch (Exception ex) {
                System.err.println(ex);
                return null;
            }
        }
        else {
            try {
                service.command(servicePath,
                                "-b", module.modulePath,
                                "-O", vectorsFile.getPath(),
                                "-g", "1").start().waitFor();
            }
            catch (Exception ex) {
                System.err.println(ex);
                return null;
            }
        }
        
        try {
            BufferedReader reader = new BufferedReader(new FileReader(vectorsFile.getPath() + ".ovec"));

            // Skip outputs header -- they should be in the same order as in the
            // module outputs array.
            reader.readLine();
            String[] outputStrings = reader.readLine().split(" ");
            reader.close();

            long[] outputLongs = new long[outputStrings.length];

            for (int i = outputStrings.length; --i >= 0;) {
                if (outputStrings[i].length() == 1) {
                    outputLongs[i] = Long.parseLong(outputStrings[i]);
                }
                else if (outputStrings[i].startsWith("0X")) {
                    outputLongs[i] = Long.parseLong(outputStrings[i].substring(2), 16);
                }
                else {
                    // TODO Handle wide output
                    throw new UnsupportedOperationException("VSIG doesn't yet support wide outputs");
                }
            }

            return outputLongs;
        }
        catch (Exception ex) {
            System.err.println(ex);
            return null;
        }
    }


    /**
     * Removes the given module from the module pool. The ID given should be an
     * ID previously received from a call to addModule. If removeModules has
     * been called since the call to addModule, this ID may no longer correspond
     * to anything.
     *
     * @param moduleID the module to be removed
     *
     * @return true if successful; false otherwise
     */
    public boolean removeModule(int moduleID)
    {
        if (!Util.inRange(moduleID, 0, rawModules.size() - 1)) {
            return false;
        }

        // Set rather than remove to maintain unique property of IDs.
        rawModules.set(moduleID, null);

        File moduleFile = new File(readyModules.remove(moduleID).modulePath);
        moduleFile.delete();

        return true;
    }


    /**
     * Removes all modules from the module pool. This will invalidate all prior
     * IDs.
     */
    public void removeModules()
    {
        for (ReadyModule module : readyModules.values()) {
            new File(module.modulePath).delete();
        }

        rawModules.clear();
        readyModules.clear();
    }


    /**
     * Performs a high-level synthesis on all modules that have been added to
     * the module pool. This will re-perform synthesis on previously processed
     * modules, so external changes to Verilog files can be incorporated without
     * readding modules.
     *
     * @return the number of modules processed
     */
    public int processModules()
    {
        if (rawModules.isEmpty()) {
            return 0;
        }

        String sep = System.getProperty("file.separator");

        for (int i = rawModules.size(); --i >= 0;) {
            String rawModuleName = new File(rawModules.get(i)).getName();
            String readyModulePath = storagePath + sep + rawModuleName + ".blif";

            service.command(servicePath,
                            "-V", rawModules.get(i),
                            "-o", readyModulePath);

            try {
                service.start().waitFor();
            }
            catch (IOException ex) {
                System.err.println(ex);
                return -1;
            }
            catch (InterruptedException ex) {
                System.err.println(ex);
                return -2;
            }

            readyModules.put(i, new ReadyModule(readyModulePath));
        }

        return readyModules.size();
    }


    /**
     * Convenience class to track information about modules that have been
     * synthesized.
     */
    private class ReadyModule
    {
        public String modulePath;
        public String[] inputs;
        public String[] outputs;


        public ReadyModule(String modulePath)
        {
            this.modulePath = modulePath;
            this.inputs = null;
            this.outputs = null;

            try {
                BufferedReader reader = new BufferedReader(new FileReader(modulePath));
                
                inputs = readHeader(reader, ".inputs");
                outputs = readHeader(reader, ".outputs");
                
                reader.close();
            }
            catch (Exception ex) {
                Logger.getLogger(ExecutorService.class.getName()).log(Level.WARNING, 
                    "Error while reading header fields.", (Object)null);
            }
        }


        private String[] readHeader(BufferedReader reader, String target)
        {
            // smithre5: This needs cleaning.
            
            if (reader == null) {
                return null;
            }
            
            String[] result = null;
            
            try {
                String line = null;
                while ((line = reader.readLine()) != null && !line.startsWith(target));
                
                if (line == null) {
                    return null;
                }
                
                LinkedHashSet<String> uniqueFields = new LinkedHashSet<String>();

                int fieldMark = 0;
                int lineLength = line.length();

                // smithre5: Magic number 8 from length of '.inputs'. This skips
                // the first token for inputs and most of the first for outputs.
                
                // Read fields from first line.
                for (int i = 8; i < lineLength; ++i) {
                    while (i < lineLength && line.charAt(i++) != FIELD_SPLIT);
                    fieldMark = i;
                    while (i < lineLength && line.charAt(i++) != FIELD_SEPARATOR);

                    if (i - fieldMark > 0) {
                        uniqueFields.add(line.substring(fieldMark, i).split(FIELD_INDEX)[0]);
                    }
                }

                // Read fields from continuing lines.
                if (line.charAt(lineLength - 1) == LINE_SPLIT) {
                    boolean readNext = true;
                    while (readNext && (line = reader.readLine()) != null) {
                        fieldMark = 0;
                        lineLength = line.length();

                        for (int i = 1; i < lineLength; ++i) {
                            while (i < lineLength && line.charAt(i++) != FIELD_SPLIT);
                            fieldMark = i;
                            while (i < lineLength && line.charAt(i++) != FIELD_SEPARATOR);

                            if (i - fieldMark > 0) {
                                uniqueFields.add(line.substring(fieldMark, i).split(FIELD_INDEX)[0]);
                            }
                        }

                        if (line.charAt(lineLength - 1) != LINE_SPLIT) {
                            readNext = false;
                        }
                    }
                }

                result = uniqueFields.toArray(new String[uniqueFields.size()]);
            }
            catch (IOException ex) {
                System.err.println(ex);
                result = null;
            }
            
            return result;
        }
        
        
        private static final char LINE_SPLIT = '\\';
        private static final char FIELD_SPLIT = '^';
        private static final char FIELD_SEPARATOR = ' ';
        private static final String FIELD_INDEX = "~";
    }


    private String servicePath;
    private String storagePath;
    private ProcessBuilder service;
    private ArrayList<String> rawModules;
    private HashMap<Integer, ReadyModule> readyModules;
}